package be.rivendale.material;

import be.rivendale.mathematics.MathematicalUtilities;
import org.apache.commons.lang.Validate;

import static java.lang.Math.min;

/**
 * Represents a color, built up out of three color components: red, green and blue.
 * Each of these components are represented as a {@link Double double} value that ranges from zero to one,
 * where one means full intensity and zero means no intensity.
 * <p>For example, creating a color like this: <code>new Color(1, 0, 0)</code> means pure red.</p>
 */
public class Color {
	/**
	 * The maximum value (intensity) of a color component (red, green or blue) when represented as an 8 bit per pixel
	 * value ranging from 0 to 255.
	 */
	private static final int MAXIMUM_8_BIT_COLOR_COMPONENT_VALUE = 255;

    /**
     * Pure red.
     */
    public static final Color RED = new Color(1, 0, 0);

    /**
     * Pure green.
     */
    public static final Color GREEN = new Color(0, 1, 0);

    /**
     * Pure blue.
     */
    public static final Color BLUE = new Color(0, 0, 1);

    /**
     * Pure black.
     */
    public static final Color BLACK = new Color(0, 0, 0);

    /**
     * Pure white.
     */
    public static final Color WHITE = new Color(1, 1, 1);
    
    /**
     * Pure yellow.
     */
    public static final Color YELLOW = new Color(1, 1, 0);

    /**
     * Represents the color cyan. Cyan is obtained by using 0% red, 100% green and  100% blue.
     */
    public static final Color CYAN = new Color(0, 1, 1);

    /**
     * Represents the color magenta. Magenta is obtained by using 100% red, 0% green and  100% blue.
     */
    public static final Color MAGENTA = new Color(1, 0, 1);

    /**
     * The red color component.
     */
    private double red;

    /**
     * The green color component.
     */
    private double green;

    /**
     * The blue color component.
     */
    private double blue;

	/**
     * Creates a new color out of the three color components
     * Combinations of these three components, yield all possible colors.
     * The components must be {@link #validateColorComponent(double) valid}.
     * @param red The red color component.
     * @param green The green color component.
     * @param blue The blue color component.
     */
    public Color(double red, double green, double blue) {
        validateColorComponents(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
    }

    /**
     * Checks that the range of the color components is within the valid range for a color component.
     * @param red The red color component to check.
     * @param green The green color component to check.
     * @param blue The blue color component to check.
     * @see #validateColorComponent(double)
     * @throws IllegalArgumentException If at least one color component is not valid.
     */
    private void validateColorComponents(double red, double green, double blue) throws IllegalArgumentException {
        validateColorComponent(red);
        validateColorComponent(green);
        validateColorComponent(blue);
    }

    /**
     * Checks that the specified color component is within the valid bounds of a color component.
     * A color component is valid when it ranges from 0..1
     * @param colorComponent The color component to check
     * @throws IllegalArgumentException If the color component does not range from 0..1
     */
    private void validateColorComponent(double colorComponent) throws IllegalArgumentException {
        Validate.isTrue(MathematicalUtilities.between(0, 1, colorComponent), "Color components must range between 0 and 1");
    }

    /**
     * Retrieves the red color component. This is guaranteed to be a value between 0 and 1.
     * @return The red color component for this color.
     */
    public double getRed() {
        return red;
    }

    /**
     * Retrieves the green color component. This is guaranteed to be a value between 0 and 1.
     * @return The green color component for this color.
     */
    public double getGreen() {
        return green;
    }

    /**
     * Retrieves the blue color component. This is guaranteed to be a value between 0 and 1.
     * @return The blue color component for this color.
     */
    public double getBlue() {
        return blue;
    }

    public boolean equals(Object object) {
        Color color = (Color)object;
        return MathematicalUtilities.equals(getRed(), color.getRed())
                && MathematicalUtilities.equals(getGreen(), color.getGreen())
                && MathematicalUtilities.equals(getBlue(), color.getBlue());
    }

    /**
     * Returns an array of length 3 containing the color components RED, GREEN and BLUE as
     * 8 bit integers (ranging from 0..255), which is the compatible pixel representation of most display and image technologies.
	 * <p><strong>Note:</strong> Converting color components represented as a double precision floating point value to
	 * 8 bit integer values inevitably causes some precision loss (Since the 8 bit values can only represent 256
	 * different shades of a color component, and a double precision floating point number can represent many more color shades.)
	 * To represent the original color as close as possible the integer values are rounded to the nearest integer step.
	 * That said, basically all current display hardware is based on 8 bit values, and can thus only display 256
	 * different shades of a color component anyway. Also, loading color information from disk is most likely already
	 * in an 8bit per subpixel format.</p>
     * <p><strong>Note:</strong> Although the actual values range from 0..255 they put into variables of type int, and
     * not of type byte. So each element in the array represents a single color subpixel!</p>
     * @return The array containing the values of RED, GREEN and BLUE as int values.
     */
    public int[] toArray() {
        return new int[] {(int) Math.round(getRed() * MAXIMUM_8_BIT_COLOR_COMPONENT_VALUE),
                (int) Math.round(getGreen() * MAXIMUM_8_BIT_COLOR_COMPONENT_VALUE),
                (int) Math.round(getBlue() * MAXIMUM_8_BIT_COLOR_COMPONENT_VALUE)
        };
    }

    /**
     * Multiplies two colors, creating a new color as the result.
     * Multiplication of two colors results in a new color whose red, green and blue components are
     * the multiplication of the corresponding red, green and blue components of the factors.
     * This always returns a darker color (except when using {@link Color#WHITE white}), since a color component ranges from 0 to 1.
     * Both this and the parameter color are left unchanged.
     * @param color The color to multiply with.
     * @return The product of the two colors.
     */
    public Color multiply(Color color) {
        Validate.notNull(color, "Unable to multiply a color with null");
        return new Color(red * color.red,
                green * color.green,
                blue * color.blue);
    }

    /**
     * Multiplies each of the color components by the specified scalar value.
     * For example, a color of {@link #RED pure red} multiplied by a scalar of 0.5 returns <code>Color(0.5, 0, 0)</code>
     * <p>If the result of the multiplication is a value large then 1, it is set to 1. It never exceeds 1.
     * Also, the specified scalar value must not be negative, since that would always end up with a value less then zero. If a negative scalar is passed, an exception is thrown</p>
     * <p>This object is left unchanged.</p>
     * @param scalarValue The scalar value to multiply each color component with.
     * @return A new color instance which contains the resulting computation.
     */
    public Color multiply(double scalarValue) {
        Validate.isTrue(scalarValue >= 0, "A color can not be multiplied with a negative scalar");
        return new Color(
                min(red * scalarValue, 1),
                min(green * scalarValue, 1),
                min(blue * scalarValue, 1)
        );
    }

    /**
     * Adds the two colors together by adding up each of it's color components, and returning a new color with that value.
     * <p>Both this color and the passed color are left unchanged</p>
     * <p>The resulting color is always a lighter (or the same) color, since color components can not be negative
     * (the some of two positive numbers is always larger or equal to the original terms. </p>
     * <p>If the adding of two color components results in a value larger then one, the summed color compont will recieve value one. The color components can never be larger then one.</p>
     * @param color The color to add to this one.
     * @return A new color containing the sum of the two colors, which is always a lighter shade (or equal in case of adding to {@link #BLACK black}.
     */
    public Color add(Color color) {
        return new Color(
                min(color.red + red, 1),
                min(color.green + green, 1),
                min(color.blue + blue, 1)
        );
    }

	/**
	 * Converts this color to an AWT color (java.awt.Color).
	 * @return The color represented as an equivalent AWT color.
	 */
	public java.awt.Color toAwtColor() {
		int[] integerRgbComponents = toArray();
		return new java.awt.Color(integerRgbComponents[0], integerRgbComponents[1], integerRgbComponents[2]);
	}
}
